﻿//************************************************************************************************
// Copyright © 2012 Steven M. Cohn. All Rights Reserved.
//
//************************************************************************************************ 

namespace CookieChipper
{
	using System;
	using System.IO;
	using System.Text;


	/// <summary>
	/// 
	/// </summary>

	internal class BackupProvider : IDisposable
	{
		private const string BackupFilename = "CookieChipper_{0}.dat";

		private string root;
		private string path;
		private FileStream stream;
		private Encoding encoding;
		private bool isDisposed;


		//========================================================================================
		// Lifecycle
		//========================================================================================

		/// <summary>
		/// Initialize a new provider with the given path.
		/// </summary>
		/// <param name="path"></param>

		public BackupProvider (string root)
		{
			this.root = root;
			this.encoding = new ASCIIEncoding();
			this.isDisposed = false;
		}


		/// <summary>
		/// Dispose the instance.
		/// </summary>

		public void Dispose ()
		{
			Dispose(true);
		}


		/// <summary>
		/// Dispose the instance.
		/// </summary>
		/// <param name="disposing"></param>

		protected virtual void Dispose (bool disposing)
		{
			if (!isDisposed)
			{
				if (stream != null)
				{
					try
					{
						stream.Dispose();
					}
					catch
					{
					}
					finally
					{
						stream = null;
					}
				}

				isDisposed = true;
			}
		}


		/// <summary>
		/// Prepare the provider for reading.
		/// </summary>
		/// <remarks>
		/// Exception may be thrown from this routine which should be caught by the consumer
		/// so helpful messages can be displayed.
		/// </remarks>

		public void OpenReader ()
		{
			this.path = Path.GetFullPath(this.root);
			stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
		}


		/// <summary>
		/// Prepare the provider for writing.
		/// </summary>
		/// <remarks>
		/// Exception may be thrown from this routine which should be caught by the consumer
		/// so helpful messages can be displayed.
		/// </remarks>

		public void OpenWriter ()
		{
			this.path = Path.Combine(root,
				String.Format(BackupFilename, DateTime.Now.ToString("yyyyMMddHHmmss")));

			stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
		}


		//========================================================================================
		// Properties
		//========================================================================================

		/// <summary>
		/// Gets the number of cookies processed.
		/// </summary>

		public int Count
		{
			get;
			private set;
		}


		/// <summary>
		/// Gets the full path of the backup file as dictated by the location
		/// passed to the constructor.
		/// </summary>

		public string FullPath
		{
			get
			{
				return path;
			}
		}


		//========================================================================================
		// Backup
		//========================================================================================

		/// <summary>
		/// Create a backup of all protected cookies.
		/// </summary>
		/// <returns></returns>

		public bool Backup ()
		{
			bool success = false;
			int count = 0;

			Oven oven = new Oven(false);

			using (var provider = new CookieCacheProvider())
			{
				if (success = provider.Open())
				{
					// write empty integer as placeholder for final count of cookies
					Write(0);

					Cookie cookie = null;
					while ((cookie = provider.GetNext()) != null)
					{
						// skip empty records in the cache index.dat
						if (cookie.Name.Length > 0)
						{
							oven.Bake(cookie);

							if ((cookie.State & CookieState.Locked) > 0)
							{
								try
								{
									using (var reader = new CookieFileProvider(cookie.Path))
									{
										reader.LoadChips(cookie);
									}

									Write(cookie);
								}
								catch (System.IO.FileNotFoundException)
								{
									// most likely a phantom
								}
							}
						}

						count++;
					}

					// return to beginning and overwrite placeholder with actual count
					stream.Position = 0;
					Write(count);

					this.Count = count;
					success = true;
				}
			}

			return success;
		}

		#region Writers

		private void Write (Cookie cookie)
		{
			Write(cookie.Name);						// 0-term
			Write(cookie.Domain);					// 0-term
			Write(cookie.Flags);					// 4
			Write(cookie.Accessed);					// 8
			Write(cookie.Updated);					// 8

			Write(cookie.Values.Count);				// 4

			foreach (var chip in cookie.Values)
			{
				Write(chip.Name);					// 0-term
				Write(chip.Value);					// 0-term
				Write(chip.Flags);					// 4
				Write(chip.Created);				// 8
				Write(chip.Expires);				// 8
			}
		}

		private void Write (ChipFlags value)
		{
			byte[] bytes = BitConverter.GetBytes((uint)value);
			stream.Write(bytes, 0, bytes.Length);
		}

		private void Write (DateTime value)
		{
			byte[] bytes = BitConverter.GetBytes(value.ToFileTime());
			stream.Write(bytes, 0, bytes.Length);
		}

		private void Write (int value)
		{
			byte[] bytes = BitConverter.GetBytes(value);
			stream.Write(bytes, 0, bytes.Length);
		}

		private void Write (string value)
		{
			byte[] bytes;

			if (value.Length == 0)
			{
				bytes = new byte[1] { 0 };
			}
			else
			{
				bytes = encoding.GetBytes(value);
				stream.Write(bytes, 0, bytes.Length);
				bytes[0] = 0;
			}

			stream.Write(bytes, 0, 1);
		}

		#endregion Writers


		//========================================================================================
		// Restore
		//========================================================================================

		/// <summary>
		/// Restore the protected cookies in the current backup file.
		/// </summary>
		/// <returns>The total number of protected cookies restored.</returns>

		public int Restore ()
		{
			int count = ReadInt();

			// 1. prepare index, creating structure if empty
			// 2. scan index for empty slots to ensure we have enough space for 'count' cookies
			// 3. clean if not enough slots
			// 4. enough slots now?
			// 5. restore cookies

			using (var provider = new CookieCacheProvider())
			{
				if (provider.Open())
				{
					//if (provider.Header.
				}
			}

			Cookie cookie;
			while ((cookie = Read()) != null)
			{
				string s = cookie.Name;
			}

			this.Count = count;
			return count;
		}

		#region Readers

		public Cookie Read ()
		{
			string name = ReadString();
			if (String.IsNullOrEmpty(name))
			{
				return null;
			}

			var cookie = new Cookie(new HashRef(), ReadString());
			cookie.Domain = ReadString();
			cookie.Flags = (ChipFlags)ReadInt();
			cookie.Accessed = ReadDateTime();
			cookie.Updated = ReadDateTime();

			int count = ReadInt();

			while (count > 0)
			{
				var chip = new Chip(ReadString());
				chip.Value = ReadString();
				chip.Flags = (ChipFlags)ReadInt();
				chip.Created = ReadDateTime();
				chip.Expires = ReadDateTime();

				cookie.Add(chip);

				count--;
			}

			return cookie;
		}


		private DateTime ReadDateTime ()
		{
			byte[] bytes = new byte[8];
			if (stream.Read(bytes, 0, 8) == 8)
			{
				return DateTime.FromFileTime(BitConverter.ToInt64(bytes, 0));
			}

			return DateTime.MinValue;
		}

		private int ReadInt ()
		{
			byte[] bytes = new byte[4];
			if (stream.Read(bytes, 0, 4) == 4)
			{
				return BitConverter.ToInt32(bytes, 0);
			}

			return Int32.MinValue;
		}

		private string ReadString ()
		{
			int b;
			var builder = new StringBuilder(10);

			while ((b = stream.ReadByte()) != 0)
			{
				builder.Append((char)b);
			}

			return builder.ToString();
		}

		#endregion Readers
	}
}
